package top.fullj.chain;

import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import top.fullj.bean.ToString;

import javax.annotation.Nonnull;
import java.util.*;

/**
 * @author bruce.wu
 * @since 2021/6/18 9:11
 */
public class ChainBase implements Chain {

    private static final String HEAD_NAME = "_HEAD_";
    private static final String TAIL_NAME = "_TAIL_";

    private final Node head;
    private final Node tail;

    public ChainBase() {
        this.head = new Head(this);
        this.tail = new Tail(this);
        this.head.next = tail;
        this.tail.prev = head;
    }

    private ChainBase(Node head, Node tail) {
        this.head = head;
        this.tail = tail;
    }

    @Override
    public final void proceed(Chain.Context ctx) throws HandlerException {
        if (head.next == tail) {
            // end of the chain
            return;
        }
        Handler handler = head.next.handler;
        handler.handle(ctx, new SubChain(head.next, tail));
    }

    @Override
    @Nonnull
    public final Chain origin() {
        return head.chain;
    }

    @Override
    @Nonnull
    public final List<String> names() {
        List<String> result = new LinkedList<>();
        Node node = head.next;
        while (node != tail) {
            result.add(node.name);
            node = node.next;
        }
        return result;
    }

    @Override
    @Nonnull
    public final Map<String, Handler> toMap() {
        Map<String, Handler> map = new LinkedHashMap<>();
        Node node = head.next;
        while (node != tail) {
            map.put(node.name, node.handler);
            node = node.next;
        }
        return Collections.unmodifiableMap(map);
    }

    @Override
    public final Iterator<Map.Entry<String, Handler>> iterator() {
        return toMap().entrySet().iterator();
    }

    @Override
    public final String toString() {
        MoreObjects.ToStringHelper helper = ToString.toStringHelper(this);
        Node node = head.next;
        while (node != tail) {
            helper.add(node.name, node.handler.getClass().getName());
            node = node.next;
        }
        return helper.toString();
    }

    @Override
    public final Chain addFirst(Handler handler) {
        return addFirst(null, handler);
    }

    @Override
    public final Chain addFirst(String name, Handler handler) {
        synchronized (this) {
            name = filterName(name, handler);
            Node node = new Node(this, name, handler);
            addFirst(node);
        }
        return this;
    }

    @Override
    public final Chain addFirst(Handler... handlers) {
        Preconditions.checkNotNull(handlers);
        for (int i = handlers.length - 1; i >= 0; i--) {
            Handler handler = handlers[i];
            if (handler != null) {
                addFirst(null, handler);
            }
        }
        return this;
    }

    @Override
    public final Chain addLast(Handler handler) {
        return addLast(null, handler);
    }

    @Override
    public final Chain addLast(Handler... handlers) {
        Preconditions.checkNotNull(handlers);
        for (Handler handler : handlers) {
            if (handler != null) {
                addLast(null, handler);
            }
        }
        return this;
    }

    @Override
    public final Chain addLast(String name, Handler handler) {
        synchronized (this) {
            name = filterName(name, handler);
            Node node = new Node(this, name, handler);
            addLast(node);
        }
        return this;
    }

    @Override
    public final Chain addBefore(String originName, Handler handler) {
        addBefore(require(originName), null, handler);
        return this;
    }

    @Override
    public final Chain addBefore(Class<? extends Handler> type, Handler handler) {
        addBefore(require(type), null, handler);
        return this;
    }

    @Override
    public final Chain addBefore(String originName, String name, Handler handler) {
        addBefore(require(originName), name, handler);
        return this;
    }

    @Override
    public final Chain addAfter(String originName, Handler handler) {
        addAfter(require(originName), null, handler);
        return this;
    }

    @Override
    public final Chain addAfter(Class<? extends Handler> type, Handler handler) {
        addAfter(require(type), null, handler);
        return this;
    }

    @Override
    public final Chain addAfter(String originName, String name, Handler handler) {
        addAfter(require(originName), name, handler);
        return this;
    }

    @Override
    public final Chain remove(Handler handler) {
        remove(require(handler));
        return this;
    }

    @Override
    public final Handler remove(String name) {
        return remove(require(name)).handler;
    }

    @Override
    public final Handler remove(Class<? extends Handler> type) {
        return remove(require(type)).handler;
    }

    @Override
    public final Handler removeFirst() {
        if (head.next == tail) {
            throw new NoSuchElementException();
        }
        return remove(head.next).handler;
    }

    @Override
    public final Handler removeLast() {
        if (tail.prev == head) {
            throw new NoSuchElementException();
        }
        return remove(tail.prev).handler;
    }

    @Override
    public final Chain replace(Handler originHandler, String name, Handler handler) {
        replace(require(originHandler), name, handler);
        return this;
    }

    @Override
    public final Handler replace(String originName, String name, Handler handler) {
        return replace(require(originName), name, handler);
    }

    @Override
    public final Handler replace(Class<? extends Handler> type, Handler handler) {
        return replace(require(type), null, handler);
    }

    @Override
    public final Handler replace(Class<? extends Handler> type, String name, Handler handler) {
        return replace(require(type), name, handler);
    }

    @Override
    public final Handler first() {
        if (head.next == tail) {
            return null;
        }
        return head.next.handler;
    }

    @Override
    public final Handler last() {
        if (head == tail.prev) {
            return null;
        }
        return tail.prev.handler;
    }

    @Override
    public final Handler get(String name) {
        Node node = context(name);
        if (node == null) {
            return null;
        }
        return node.handler;
    }

    @Override
    public final Handler get(Class<? extends Handler> type) {
        Node node = context(type);
        if (node == null) {
            return null;
        }
        return node.handler;
    }

    private void addFirst(Node node) {
        Node next = head.next;
        node.prev = head;
        node.next = next;
        head.next = node;
        next.prev = node;
    }

    private void addLast(Node node) {
        Node prev = tail.prev;
        node.prev = prev;
        node.next = tail;
        prev.next = node;
        tail.prev = node;
    }

    private synchronized void addBefore(Node target, String name, Handler handler) {
        name = filterName(name, handler);
        Node node = new Node(this, name, handler);
        addBefore(target, node);
    }

    private void addBefore(Node target, Node node) {
        node.prev = target.prev;
        node.next = target;
        target.prev.next = node;
        target.prev = node;
    }

    private synchronized void addAfter(Node target, String name, Handler handler) {
        name = filterName(name, handler);
        Node node = new Node(this, name, handler);
        addAfter(target, node);
    }

    private void addAfter(Node target, Node node) {
        node.prev = target;
        node.next = target.next;
        target.next.prev = node;
        target.next = node;
    }

    private Handler replace(Node origin, String name, Handler handler) {
        assert origin != head && origin != tail;
        if (name == null) {
            name = generateName(handler);
        }
        synchronized (this) {
            if (!origin.name.equals(name)) {
                checkNotDuplicate(name);
            }
            Node node = new Node(this, name, handler);
            replace(origin, node);
        }
        return origin.handler;
    }

    private void replace(Node origin, Node node) {
        Node prev = origin.prev;
        Node next = origin.next;
        node.prev = prev;
        node.next = next;

        prev.next = node;
        next.prev = node;

        origin.prev = node;
        origin.next = node;
    }

    private synchronized Node remove(Node node) {
        assert node != head && node != tail;

        Node prev = node.prev;
        Node next = node.next;
        prev.next = next;
        next.prev = prev;
        return node;
    }

    private Node context(String name) {
        Preconditions.checkNotNull(name);
        Node node = head.next;
        while (node != tail) {
            if (node.name.equals(name)) {
                return node;
            }
            node = node.next;
        }
        return null;
    }

    private Node context(Handler handler) {
        Preconditions.checkNotNull(handler);
        Node node = head.next;
        while (node != tail) {
            if (node.handler == handler) {
                return node;
            }
            node = node.next;
        }
        return null;
    }

    private Node context(Class<? extends Handler> type) {
        Preconditions.checkNotNull(type);
        Node node = head.next;
        while (node != tail) {
            if (type.isAssignableFrom(node.handler.getClass())) {
                return node;
            }
            node = node.next;
        }
        return null;
    }

    private Node require(String name) {
        Node node = context(name);
        if (node == null) {
            throw new NoSuchElementException(name);
        }
        return node;
    }

    private Node require(Class<? extends Handler> type) {
        Node node = context(type);
        if (node == null) {
            throw new NoSuchElementException(type.getName());
        }
        return node;
    }

    private Node require(Handler handler) {
        Node node = context(handler);
        if (node == null) {
            throw new NoSuchElementException(handler.getClass().getName());
        }
        return node;
    }

    private String filterName(String name, Handler handler) {
        if (name == null) {
            return generateName(handler);
        }
        checkNotDuplicate(name);
        return name;
    }

    private void checkNotDuplicate(String name) {
        if (context(name) != null) {
            throw new IllegalArgumentException("duplicate handler name: " + name);
        }
    }

    private String generateName(Handler handler) {
        String prefix = ToString.simpleClassName(handler.getClass()) + "#";
        String name = prefix + "0";
        if (context(name) != null) {
            for (int i = 1; ; i++) {
                name = prefix + i;
                if (context(name) == null) {
                    break;
                }
            }
        }
        return name;
    }

    private static class Node {

        private volatile Node prev;
        private volatile Node next;

        private final Chain chain;
        // name of the handler
        private final String name;
        private final Handler handler;

        Node(@Nonnull Chain chain, @Nonnull String name, Handler handler) {
            this.chain = chain;
            this.name = name;
            this.handler = handler;
        }

    }

    private static final class Head extends Node {
        Head(Chain chain) {
            super(chain, HEAD_NAME, null);
        }
    }

    private static final class Tail extends Node {
        Tail(Chain chain) {
            super(chain, TAIL_NAME, null);
        }
    }

    private static final class SubChain extends ChainBase {
        SubChain(Node head, Node tail) {
            super(head, tail);
        }
    }

}
